VGA-Kurs - Part #4 Wir kommen nun zu "T.C.P.'s Beginner's Guide to VGA Coding", Part IV. Noch eine Anmerkung: Ich versuche immer, auch Lesern, die die ersten Teile des Kurses verpat haben, die Mglichkeit zu geben, die Beispiele zu nutzen, auch wenn ihnen die wichtigen Prozeduren fehlen. Der Nachteil ist, da ich z.B. die schnelle PutPixel-Prozedur durch einen langsameren Speicherzugriff via Mem ersetzen mu. Nun eine Frage: Soll ich es so beibehalten oder in jedem Teil die bentigten Prozeduren dazuschreiben? Fr alle, die die ersten Teile des Kurses haben: Ihr solltet auf jeden Fall folgende Prozeduren immer bereit halten: PutPixel, WaitRetrace, ClrVGA. Ihr knnt diese Prozeduren dann in die Listings einsetzen. In diesem Teil werden wir Sprites behandeln, als Bonus gibt es einen kleinen Abschnitt ber Code-Optimierung. Auf dem PC sind Sprites immer noch fr viele ein Mythos, die vorher auf Homecomputern wie Amiga/Atari/C64 gecodet haben, wo die Maschine alle Sprite-Operationen erledigt. Beim PC dagegen war am Anfang nie vorgesehen, da er einmal auch nur ein Sprite ber den Bildschirm flitzen lassen wrde. Deshalb baute man solch eine Funktion auch nie in die PC-Grafikkarten ein. Das Ergebnis ist, da wenn man auf der VGA-Karte Sprites zaubern will, sich um alles selbst zu kmmern hat: Darstellung, Bewegung, Kollisionsabfragen, Clipping, Durchscheinen des Hintergrundes, runde Sprites etc. Das dies erheblich auf die CPU geht, ist abzusehen, denn die VGA-Karte trgt kein bischen dazu bei. Allerdings kann man sich einiger Tricks behelfen, wie wir noch sehen werden. Zu allererst sollte man sich einen Sprite-Editor zulegen. Davon gibt es unzhlige als Shareware, man kann aber auch ein Malprogram wie DPaint heranziehen. Nun mu man die Sprites in ein Format bekommen, das man von Pascal aus leicht lesen kann. Hierbei ist es gnstig, wenn man einen Sprite-Editor hat, der die Sprites als RAW-Dump abspeichert. D.h., da die Pixel-Informationen hintereinander in einer Datei abgelegt werden. Zeichnet man z.B. ein 32x32 Pixel groes Sprite, so hat man eine 1024 Byte groe Datei, die man direkt einlesen kann: type Sprite = array[0..1023] of byte; var f : file; Spr : Sprite; procedure Readsprite(str:string;s:Sprite); begin assign(f,str); reset(f,1); blockread(f,s,1024); close(f); end; Diese Prozedur liest durch die Blockread-Prozedur 1024 Byte aus der Datei mit dem Namen Str in die bergebene Variable des Typs Sprite. Anmerkung: Wer den Autodesk Animator besitzt, kann die Sprites im CEL-Format speichern. Dieses hat allerdings noch einen 800 Byte groen Header, der per "seek(f,800)" bersprungen werden mu. Es gibt aber auch Sprite-Editoren, die die Sprites gleich als Pascal-Source speichern knnen (z.B. YC's Sprite Editor, den ich benutze). Dies hat dann die folgende Form: const Spr : Sprite = ( 0,1,2,3,5,128,255,200,50,20,... ); Nun, da wir hoffentlich unser Sprite in der Variablen Spr haben, wird es Zeit, es auf den Bildschirm zu bringen: procedure ShowSprite(x,y:word;s:Sprite); var n1,n2 : byte; begin for n1 := 0 to 31 do for n2 := 0 to 31 do mem[$A000:(n1+y)*320+x+n2] := s[n1*32+n2]; end; Dies ist die einfachste Form der ShowSprite-Routine. Kein Clipping, kein Durchscheinen, keine runden Sprites und das Wichtigste: Keine Geschwindigkeit! Hier die Funktionsweise der Prozedur: In der Schleife wird ein 32x32 Pixel groes Sprite an die Koordinaten (X,Y) gesetzt. Dazu wird die Adresse im Bildschirmspeicher nach der bekannten Formel Adresse = 320 * Y + X berechnet und dann die Position im Sprite-Array mittels Position = 32 * n1 + n2 dazugezhlt. Daraus ergibt sich die Formel Adresse = (n1+Y) * 320 + X + n2. Schn und gut, aber was ist, wenn das Sprite rund ist, z.B. ein Fuball. Wenn ich das Sprite auf einen schwarzen Hintergrund setze, macht es nichts, aber wenn ich einen Hintergrund, wie z.B. ein Fuballfeld habe, und das Sprite darauf setze, dann habe ich um den Ball einige schwarze Pixel. Das liegt daran, da das Array, in dem das Sprite liegt, quadratisch ist, und wenn nun im Sprite zwar kein Pixel vorgesehen ist, also im Array eine 0 steht, dann wird trotzdem ein Pixel der Farbe 0 gesetzt. Noch ein Problem: Hat man ein Sprite, da an gewissen Stellen durchsichtig ist, z.B. eine Scheibe Schweizer Kse (Schei Beispiel, ich wei), dann sollte an den Stellen, wo die Lcher sind, der Hintergrund durchscheinen, tut er aber nicht, da wieder ein Pixel der Farbe 0 gesetzt wird. Diese Probleme sind noch leicht zu lsen: procedure ShowSprite(x,y:word;s:Sprite); var n1,n2 : byte; begin for n1 := 0 to 31 do for n2 := 0 to 31 do if s[n1*32+n2] <> 0 then mem[$A000:(n1+y)*320+x+n2] := s[n1*32+n2]; end; Findet die Routine nun im Sprite ein Pixel der Farbe 0, so wird es erst gar nicht gesetzt. Dadurch wird die Prozedur sogar schneller, da einige Pixel des Sprites bersprungen werden, und so Zeit gespart wird. Ein weiteres Problem ist, da wenn das Sprite am Rand des Bildschirms angelangt ist und ber den Rand hinausgeht, es an der anderen Seite des Bildschirms dargestellt wird. Das liegt am linearen Aufbau des Bildschirmspeichers. Gehen die Spriteinformationen ber den Rand einer Zeile hinaus, werden sie im Bildschirmspeicher weiter bewegt und erscheinen dadurch in der nchsten Zeile. Dasselbe passiert, wenn das Sprite den unteren Rand des Bildschirms berschreitet. Ein Verfahren zum Vereiteln dieser Tatsache ist, da vor dem Setzen eines Pixels berprft wird, ob er berhaupt noch in der Zeile bzw. Spalte liegt, also X nicht grer als 319 und Y nicht grer als 199 ist. Dies nennt sich Clipping. procedure ShowSprite(x,y:word;s:Sprite); var n1,n2 : byte; begin for n1 := 0 to 31 do for n2 := 0 to 31 do if (s[n1*32+n2] <> 0) and (n2+x < 320) and (n1+y < 200) then mem[$A000:(n1+y)*320+x+n2] := s[n1*32+n2]; end; Damit enthlt unsere Prozedur schon eine Menge Ifs und ist deshalb nicht gerade als die schnellste zu bezeichnen. Anders wre das schon bei der Assembler-Version, die zieht sich allerdings etwas in die Lnge... procedure ShowSprite(x,y,add:word);assembler; asm mov bx,31 { Zhler mit Endwert initialisieren } @loop1: { fr ein 32x32 Pixel Sprite } mov cx,31 @loop2: mov si,bx { n1 * 32 + n2 } shl si,5 { si * 32 } add si,cx add si,add { Variablen-Offset drauf } cmp byte ptr ds:[si],0 { Pixelwert = 0 ? } je @next { Wenn ja, nchster Pixel } mov ax,cx add ax,x cmp ax,319 { X-Koordinate > 319 ? } ja @next { Wenn ja, nchster Pixel } mov ax,bx add ax,y cmp ax,199 { Y-Koordinate > 199 ? } ja @next { Wenn ja, nchster Pixel } mov ax,bx { (n1+y) * 320 + x + n2 } add ax,y mov dx,ax shl ax,6 { ax * 64 } shl dx,8 { dx * 256 } add ax,dx add ax,x add ax,cx mov di,ax mov ax,0A000h { VGA-Segment nach ES } mov es,ax mov al,ds:[si] { Pixelwert aus Sprite-Daten holen } mov es:[di],al { und auf VGA-Screen setzen } @next: dec cx { Zhler dekrementieren } jnz @loop2 { Wenn ungleich 0, innere Schleife } dec bx { Zhler dekrementieren } jnz @loop1 { Wenn ungleich 0, uere Schleife } end; Diese Routine schafft immerhin schon rund 3000 Sprites pro Sekunde auf einem DX/2 80, also ca. doppelt so viel wie die Pascal-Version. Aber es geht (natrlich) noch schneller. Die Vorgehensweise der Routine entnehmt ihr bitte den Kommentaren. Folgende Tricks wurden in dieser Routine zur Optimierung verwendet: 1. Es wird nur das Offset des Sprites bergeben. Statt das gesamte Sprite als Array an die Prozedur zu bergeben, wird ihr nur das Offset des Sprites im Speicher mitgeteilt. Dadurch spart man es sich, ganze 1022 Byte mehr zu bergeben. Der Prozeduraufruf mu also nicht 'showsprite(x,y,Spritename)' lauten, sondern 'showsprite(x,y,ofs(Spritename))'. 2. Es werden so viel Register wie mglich ausgenutzt. Dies ist eine goldene Regel in der Assembler-Programmierung. Man sollte erst auf Variablen zurckgreifen, wenn wirklich alle Register ausgenutzt sind. 3. Die Zhler werden dekrementiert. Am Anfang werden die Zhler nicht mit dem Start- sondern mit dem Endwert initialisiert. Danach werden sie in jeder Schleife dekrementiert. Dies hat den Vorteil, das man sich ein langsames 'cmp zaehler,Endwert' erspart. Es gengt der bedingte Sprungbefehl 'jnz', der automatisch erneut zum Anfang der Schleife springt, wenn der Zhler ungleich 0 ist. 4. Statt 'mul' oder 'div' kommt 'shl' oder 'shr'. Hat man eine Multiplikation bzw. Division mit bzw. durch einen Faktor der ein vielfaches von 2 darstellt, kann man die Operation durch Bitverschiebung beschleunigen. OK, wer hat den Satz verstanden? Niemand? Gut. Also: Angenommen, man mu (wie in der Prozedur) eine Zahl mit 32 multiplizieren. Dies geht vor allem auf Prozessoren unter 486 recht trge vonstatten. Schneller geht es, wenn man die Zahl um 5 nach links shiftet, d.h. alle Bits der Zahl um 5 Stellen nach links verschiebt. Denn: Eine Verschiebung um 1 Bit erzeugt dasselbe Ergebnis wie eine Multiplikation mit 2. Ein Shiften um 2 Bits ist wie ein Malnehmen mit 4, 3 Bits wie mal 8, 4 Bits wie mal 16, usw. Andersrum geht's auch. Ein Shiften um 1 Bit nach rechts ist wie eine Division durch 2, wobei immer abgerundet wird. Andere Tricks: 1. Exklusiv-Oder geschickt einsetzen. Ein 'mov ax,0' ist identisch mit 'xor ax,ax', blo mit dem Unterschied, da die zweite Art wesentlich schneller geht und als OpCode weniger Bytes verbraucht. 2. So viel wie mglich raus aus der Schleife. In Schleifen sollte nur das stehen, was dort auch wirklich hingehrt. Wenn Schleifen unntige Befehle wie Variablenzuweisungen oder Rechenoperationen enthalten, die auch auerhalb der Schleife ihren Dienst verrichten knnen, sollte man sie rauswerfen. 3. Compiler-Optionen ausnutzen. Folgende Compiler-Befehle am Anfang des Codes beschleunigen das Programm: {$G+,D-,I-,Q-,R-,S-} Damit wird alles, was bremst, abgeschaltet: Debug-Informationen, In/Out-, Range- und Stack-Checking. So, schn und gut, aber was ist, wenn wir unser Sprite ber einen Hintergrund bewegen wollen? Also, wir setzen das Sprite auf unseren Hintergrund und lschen es wieder, um es danach an eine andere Stelle zu setzen. Wuups, da ist ja jetzt ein Loch in unserem schnen Hintergrund! Tja, um dies zu lsen, knnte man sich Methode 1 oder 2 bedienen. Methode 1 ist, den Hintergrund, auf den das Sprite gesetzt wird, vor dem Setzen zu sichern, und danach wieder herzustellen, wie in folgendem Schema: var Buffer : Sprite; n1,n2 : byte; begin setmcgamode; ErzeugeHintergrund; repeat for n1 := 0 to 31 do { Ausschnitt sichern } for n2 := 0 to 31 do Buffer[n1*32+n2] := mem[$A000:(n1+y)*320+x+n2]; showsprite(x,y,ofs(MySprite)); { Sprite zeichnen } showsprite(x,y,ofs(Buffer)); { HG wiederherstellen } BewegeSprite; { Neue Koordinaten } until Bedingung = true; settextmode; end. Gut, fr ein Sprite mag diese Methode ausreichen, aber was ist, wenn man z.B. 10 Sprites ber den HG bewegen will? Tja, jetzt mu Methode 2 herhalten. Diese benutzt folgendes Schema: begin setmcgamode; ErzeugeHintergrundAufVS; repeat KopiereHGAufVGA; ZeichneSprites; until Bedingung = true; settextmode; end. Moment maaal! Was soll den 'VS' sein? Nun, das ist der Virtual Screen. Also ein 'virtueller Bildschirm'. Man kann ihn beschreiben und auslesen wie die VGA. Aber das Wichtige ist: Man kann ihn auf die VGA kopieren, so, da sein Inhalt auf dem Bildschirm erscheint. Man erstellt also einen Virtual Screen (wie, werden wir noch sehen), und erstellt einen Hintergrund auf ihm. Dann startet man die Schleife und kopiert den Hintergrund aus dem VS auf die VGA. Nun zeichnet man seine Sprites darauf. Aber wie erstelle ich nun so ein Teil??? Am einfachsten wre wohl die folgende Lsung: var VS : array[0..63999] of byte; Diese Variable knnte nun wie ein zweiter VGA-Bildschirm benutzt werden, aber: Das Array ist 64000 Byte gro. Da man aber unter Pascal fr globale Variablen nur maximal 65000 und ein paar Byte zur Verfgung hat, wrde es sehr eng werden, und schnell kriegen wir die Compiler-Meldung 'Zuviele Variablen'. Die Lsung fr unser Problem sind also: Zeiger (Pointer). Die Deklaration des VS mu also so aussehen: type BigArr = array[0..63999] of byte; VSPtr = ^BigArr; var VS : VSPtr; VSAdd : word; Nun mssen wir den VS nur noch initialisieren: procedure InitVS; begin getmem(VS,64000); VSAdd := seg(VS^); end; Nun haben wir fr den VS 64000 Byte an Speicher allokiert. Um ihn wieder freizugeben sollte folgende Prozedur benutzt werden: procedure CloseVS; begin freemem(VS,64000); end; Will man nun auf den VS statt auf die VGA schreiben, ersetzt man die Adresse des Bildschirmspeichers 'A000h:0' durch 'VSAdd:0'. Mit dem Mem-Befehl she das so aus: x := 50; y := 80; mem[VSAdd:y*320+x] := Col; setzt auf dem VS an die Koordinaten (50,80) einen Pixel der Farbe Col. Was aber ntzt mir das? Nun, nachdem wir den Hintergrund auf diese Weise auf den VS gezeichnet haben, knnen wir ihn mit der folgenden Prozedur auf den VGA-Screen kopieren: procedure Flip;assembler; asm push ds { DS MU gesichert werden } mov ax,0A000h { Zielsegment nach ES } mov es,ax mov ds,VSAdd { Quellsegment nach DS } xor si,si { Offset = 0 } xor di,di mov cx,32000 { 32000 Word (=64000 Byte) } rep movsw { kopieren } pop ds { DS wiederherstellen } end; Nun werden, wie besprochen, die Sprites darber gezeichnet, was natrlich entsprechend flott vonstatten gehen sollte. Um Flickern und 'zerrissene' Sprites zu vermeiden, sollte man auerdem noch die WaitRetrace-Prozedur aufrufen. Wer das mit dem Virtual Screen noch nicht ganz gepeilt hat (ist nicht einfach, geb ich zu), der sei getrstet, ab der nchsten Ausgabe brauchen wir so was nicht mehr, denn dann besprechen wir den VGA-Modus, der von Hause aus 4 Virtual Screens mitbringt, ohne auch nur ein Byte mehr Speicher zu verbrauchen: Der Mode-X! Also, ciao bis zum nchsten Teil. [ This text copyright (c) 1995-96 Johannes Spohr. All rights reserved. ] [ Distributed exclusively through PC-Heimwerker, Verlag Thomas Eberle. ] [ ] [ No part of this document may be reproduced, transmitted, ] [ transcribed, stored in a retrieval system, or translated into any ] [ human or computer language, in any form or by any means; electronic, ] [ mechanical, magnetic, optical, chemical, manual or otherwise, ] [ without the expressed written permission of the author. ] [ ] [ The information contained in this text is believed to be correct. ] [ The text is subject to change without notice and does not represent ] [ a commitment on the part of the author. ] [ The author does not make a warranty of any kind with regard to this ] [ material, including, but not limited to, the implied warranties of ] [ merchantability and fitness for a particular purpose. The author ] [ shall not be liable for errors contained herein or for incidental or ] [ consequential damages in connection with the furnishing, performance ] [ or use of this material. ]